# DMLGenerator.java

    package org.codefilarete.stalactite.sql.statement;

    import java.util.Collection;
    import java.util.Comparator;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.TreeSet;

    import org.codefilarete.stalactite.engine.runtime.DMLExecutor;
    import org.codefilarete.stalactite.mapping.Mapping.UpwhereColumn;
    import org.codefilarete.stalactite.query.builder.DMLNameProvider;
    import org.codefilarete.stalactite.query.model.Fromable;
    import org.codefilarete.stalactite.query.model.Selectable;
    import org.codefilarete.stalactite.sql.DMLNameProviderFactory;
    import org.codefilarete.stalactite.sql.ddl.DDLAppender;
    import org.codefilarete.stalactite.sql.ddl.structure.Column;
    import org.codefilarete.stalactite.sql.ddl.structure.Table;
    import org.codefilarete.stalactite.sql.statement.binder.ParameterBinder;
    import org.codefilarete.stalactite.sql.statement.binder.ParameterBinderIndex;
    import org.codefilarete.tool.Strings;
    import org.codefilarete.tool.bean.Objects;
    import org.codefilarete.tool.collection.Iterables;
    import org.codefilarete.tool.collection.Sorter;
    import org.codefilarete.tool.trace.MutableInt;

    import static org.codefilarete.stalactite.sql.statement.ExpandableSQL.ExpandableParameter.SQL_PARAMETER_MARK;
    import static org.codefilarete.stalactite.sql.statement.ExpandableSQL.ExpandableParameter.SQL_PARAMETER_MARK_1;
    import static org.codefilarete.stalactite.sql.statement.ExpandableSQL.ExpandableParameter.SQL_PARAMETER_MARK_10;
    import static org.codefilarete.stalactite.sql.statement.ExpandableSQL.ExpandableParameter.SQL_PARAMETER_MARK_100;
    import static org.codefilarete.stalactite.sql.statement.ExpandableSQL.ExpandableParameter.SQL_PARAMETER_SEPARATOR;

    /**
     * Class for DML generation dedicated to {@link DMLExecutor}. Not expected to be used elsewhere.
     *
     * @author Guillaume Mary
     */
    public class DMLGenerator {

    	private static final String AND = " and ";

    	private static final String EQUAL_SQL_PARAMETER_MARK_AND = " = " + SQL_PARAMETER_MARK + AND;

    	protected final ParameterBinderIndex<Column, ParameterBinder> columnBinderRegistry;

    	protected Sorter<Column> columnSorter;

    	protected final DMLNameProviderFactory dmlNameProviderFactory;

    	public DMLGenerator(ParameterBinderIndex<Column, ParameterBinder> columnBinderRegistry, Sorter<Column> columnSorter, DMLNameProviderFactory dmlNameProviderFactory) {
    		this.columnBinderRegistry = columnBinderRegistry;
    		setColumnSorter(columnSorter);
    		this.dmlNameProviderFactory = dmlNameProviderFactory;
    	}

    	public ParameterBinderIndex<Column, ParameterBinder> getColumnBinderRegistry() {
    		return columnBinderRegistry;
    	}

    	public void setColumnSorter(Sorter<Column> columnSorter) {
    		// by default, no sort is made to avoid superfluous time consumption
    		this.columnSorter = Objects.preventNull(columnSorter, NoopSorter.INSTANCE);
    	}

    	/**
    	 * Applies an alphabetical sorter on {@link Column}s given to DML generating methods.
    	 * Made to do steady checks on SQL orders generated in tests but can be used in production.
    	 *
    	 * @see CaseSensitiveSorter#INSTANCE
    	 */
    	public void sortColumnsAlphabetically() {
    		this.setColumnSorter(CaseSensitiveSorter.INSTANCE);
    	}

    	/**
    	 * Creates a SQL statement order for inserting some data in a table.
    	 * Signature is made so that only columns of the same table can be used.
    	 *
    	 * @param columns columns that must be inserted, at least 1 element
    	 * @param <T> table type
    	 * @return a (kind of) prepared statement
    	 */
    	public <T extends Table<T>> ColumnParameterizedSQL<T> buildInsert(Iterable<? extends Column<T, ?>> columns) {
    		Iterable<Column> sortedColumns = sort(columns);
    		Table table = Iterables.first(sortedColumns).getTable();
    		DDLAppender sqlInsert = new DDLAppender(dmlNameProviderFactory.build(Fromable::getAbsoluteName), "insert into ", table, "(");
    		sqlInsert.ccat(sortedColumns, ", ");
    		sqlInsert.cat(") values (");

    		Map<Column<T, ?>, int[]> columnToIndex = new HashMap<>();
    		Map<Column<T, ?>, ParameterBinder<?>> parameterBinders = new HashMap<>();
    		MutableInt positionCounter = new MutableInt(1);
    		Iterables.stream(sortedColumns).forEach(column -> {
    			if (column.isAutoGenerated()) {
    				sqlInsert.cat("default");
    				// adding the auto-generated keys to the result is not mandatory for many database vendor, but some,
    				// as Oracle, require to specify which column must be returned in generated keys. Hence we add it
    				// to the result which makes it accessible through ColumnParameterizedSQL.getColumnIndexes
    				// Note that this column doesn't participate to position counter
    				columnToIndex.put(column, new int[] {});
    			} else {
    				sqlInsert.cat(SQL_PARAMETER_MARK);
    				columnToIndex.put(column, new int[] { positionCounter.getValue() });
    				positionCounter.increment();
    				parameterBinders.put(column, columnBinderRegistry.getBinder(column));
    			}
    			sqlInsert.cat(SQL_PARAMETER_SEPARATOR);
    		});
    		sqlInsert.cutTail(SQL_PARAMETER_SEPARATOR.length()).cat(")");
    		return new ColumnParameterizedSQL<>(sqlInsert.toString(), columnToIndex, parameterBinders);
    	}

    	/**
    	 * Creates a SQL statement order for updating some database rows depending on a where clause.
    	 * Signature is made so that only columns of the same table can be used.
    	 *
    	 * @param columns columns that must be updated, at least 1 element
    	 * @param where columns to use for where clause
    	 * @param <T> table type
    	 * @return a (kind of) prepared statement
    	 */
    	public <T extends Table<T>> PreparedUpdate<T> buildUpdate(Iterable<? extends Column<T, ?>> columns, Iterable<? extends Column<T, ?>> where) {
    		Iterable<Column> sortedColumns = sort(columns);
    		Table table = Iterables.first(sortedColumns).getTable();
    		DDLAppender sqlUpdate = new DDLAppender(dmlNameProviderFactory.build(Fromable::getAbsoluteName), "update ", table, " set ");
    		Map<UpwhereColumn<T>, Integer> upsertIndexes = new HashMap<>(10);
    		Map<UpwhereColumn<T>, ParameterBinder<?>> parameterBinders = new HashMap<>();
    		int positionCounter = 1;
    		for (Column<T, Object> column : sortedColumns) {
    			sqlUpdate.cat(column, " = " + SQL_PARAMETER_MARK_1);
    			UpwhereColumn<T> upwhereColumn = new UpwhereColumn<>(column, true);
    			upsertIndexes.put(upwhereColumn, positionCounter++);
    			parameterBinders.put(upwhereColumn, columnBinderRegistry.getBinder(column));
    		}
    		sqlUpdate.cutTail(2);
    		if (where.iterator().hasNext()) {
    			sqlUpdate.cat(" where ");
    			for (Column<T, ?> column : where) {
    				sqlUpdate.cat(column, EQUAL_SQL_PARAMETER_MARK_AND);
    				UpwhereColumn<T> upwhereColumn = new UpwhereColumn<>(column, false);
    				upsertIndexes.put(upwhereColumn, positionCounter++);
    				parameterBinders.put(upwhereColumn, columnBinderRegistry.getBinder(column));
    			}
    			sqlUpdate.cutTail(AND.length());
    		}
    		return new PreparedUpdate<>(sqlUpdate.toString(), upsertIndexes, parameterBinders);
    	}

    	/**
    	 * Creates a SQL statement order for deleting some database rows depending on a where clause.
    	 * Signature is made so that only columns of the same table can be used.
    	 *
    	 * @param table deletion target table
    	 * @param where columns to use for where clause
    	 * @param <T> table type
    	 * @return a (kind of) prepared statement parameterized by {@link Column}
    	 */
    	public <T extends Table<T>> ColumnParameterizedSQL<T> buildDelete(T table, Iterable<? extends Column<T, ?>> where) {
    		DDLAppender sqlDelete = new DDLAppender(dmlNameProviderFactory.build(Fromable::getAbsoluteName), "delete from ", table);
    		sqlDelete.cat(" where ");
    		ParameterizedWhere<T> parameterizedWhere = appendWhere(sqlDelete, where);
    		sqlDelete.cutTail(5);
    		return new ColumnParameterizedSQL<>(sqlDelete.toString(), parameterizedWhere.indexesPerColumn, parameterizedWhere.parameterBinders);
    	}

    	/**
    	 * Creates a SQL statement order for deleting some database rows by key (with a "in (?, ?, ? ...)")
    	 * Signature is made so that only columns of the same table can be used.
    	 *
    	 * @param table deletion target table
    	 * @param keyColumns key columns to use for where clause
    	 * @param whereValuesCount number of parameter in where clause (ie number of key values in where)
    	 * @param <T> table type
    	 * @return a (kind of) prepared statement parameterized by {@link Column}
    	 */
    	@SuppressWarnings("squid:ForLoopCounterChangedCheck")
    	public <T extends Table<T>> ColumnParameterizedSQL<T> buildDeleteByKey(T table, Collection<Column<T, ?>> keyColumns, int whereValuesCount) {
    		DDLAppender sqlDelete = new DDLAppender(dmlNameProviderFactory.build(Fromable::getAbsoluteName), "delete from ", table, " where ");
    		ParameterizedWhere parameterizedWhere = appendTupledWhere(sqlDelete, keyColumns, whereValuesCount);
    		Map<Column<T, ?>, int[]> columnToIndex = parameterizedWhere.getColumnToIndex();
    		Map<Column<T, ?>, ParameterBinder<?>> parameterBinders = parameterizedWhere.getParameterBinders();
    		return new ColumnParameterizedSQL<>(sqlDelete.toString(), columnToIndex, parameterBinders);
    	}

    	/**
    	 * Creates a SQL statement order for selecting some database rows depending on a where clause.
    	 * Signature is made so that only columns of the same table can be used.
    	 *
    	 * @param table selection target table
    	 * @param where columns to use for where clause
    	 * @param <T> table type
    	 * @return a (kind of) prepared statement parameterized by {@link Column}
    	 */
    	public <T extends Table<T>> ColumnParameterizedSQL<T> buildSelect(T table, Iterable<? extends Column<T, ?>> columns, Iterable<? extends Column<T, ?>> where) {
    		Iterable<Column> sortedColumns = sort(columns);
    		DDLAppender sqlSelect = new DDLAppender(dmlNameProviderFactory.build(Fromable::getAbsoluteName), "select ");
    		sqlSelect.ccat(sortedColumns, ", ");
    		sqlSelect.cat(" from ", table);
    		ParameterizedWhere<T> parameterizedWhere;
    		if (where.iterator().hasNext()) {
    			sqlSelect.cat(" where ");
    			parameterizedWhere = appendWhere(sqlSelect, where);
    			sqlSelect.cutTail(5);
    		} else {
    			parameterizedWhere = new ParameterizedWhere<>();
    		}
    		return new ColumnParameterizedSQL<>(sqlSelect.toString(), parameterizedWhere.indexesPerColumn, parameterizedWhere.parameterBinders);
    	}

    	/**
    	 * Creates a SQL statement order for selecting some database rows by key (with a "in (?, ?, ? ...)")
    	 * Signature is made so that only columns of the same table can be used.
    	 *
    	 * @param table selection target table
    	 * @param keyColumns key columns to use for where clause
    	 * @param whereValuesCount number of parameter in where clause (ie number of key values in where)
    	 * @param <T> table type
    	 * @return a (kind of) prepared statement parameterized by {@link Column}
    	 */
    	public <T extends Table<T>> ColumnParameterizedSelect<T> buildSelectByKey(T table, Iterable<? extends Column<T, ?>> columns, Collection<Column<T, ?>> keyColumns, int whereValuesCount) {
    		Iterable<Column> sortedColumns = sort(columns);
    		DMLNameProvider dmlNameProvider = dmlNameProviderFactory.build(Fromable::getAbsoluteName);
    		DDLAppender sqlSelect = new DDLAppender(dmlNameProvider, "select ");
    		Map<Selectable<?>, ParameterBinder<?>> selectParameterBinders = new HashMap<>();
    		for (Column column : sortedColumns) {
    			sqlSelect.cat(column, ", ");
    			selectParameterBinders.put(column, columnBinderRegistry.getBinder(column));
    		}
    		sqlSelect.cutTail(2).cat(" from ", table, " where ");
    		ParameterizedWhere parameterizedWhere = appendTupledWhere(sqlSelect, keyColumns, whereValuesCount);
    		Map<Column<T, ?>, int[]> columnToIndex = parameterizedWhere.getColumnToIndex();
    		Map<Column<T, ?>, ParameterBinder<?>> parameterBinders = parameterizedWhere.getParameterBinders();
    		return new ColumnParameterizedSelect<T>(sqlSelect.toString(), columnToIndex, parameterBinders, selectParameterBinders);
    	}

    	protected Iterable<Column> sort(Iterable<? extends Column> columns) {
    		return this.columnSorter.sort(columns);
    	}

    	public static class NoopSorter implements Sorter<Column> {

    		public static final NoopSorter INSTANCE = new NoopSorter();

    		@Override
    		public Iterable<Column> sort(Iterable<? extends Column> columns) {
    			return (Iterable<Column>) columns;
    		}
    	}

    	/**
    	 * Sorts columns passed as arguments.
    	 * Optional action since it's used in appending String operation.
    	 * Useful for unit tests and debug operations : give steady (and logical) column places in SQL, without this, order
    	 * is given by Column hashcode and HashMap algorithm.
    	 */
    	public static class CaseSensitiveSorter<C extends Column> implements Sorter<C> {

    		public static final CaseSensitiveSorter INSTANCE = new CaseSensitiveSorter();

    		@Override
    		public Iterable<C> sort(Iterable<? extends C> columns) {
    			TreeSet<C> result = new TreeSet<>(ColumnNameComparator.INSTANCE);
    			for (C column : columns) {
    				result.add(column);
    			}
    			return result;
    		}
    	}

    	/**
    	 * Appends a where condition (without "where" keyword) to a given sql order.
    	 * Conditions are appended with the form of ands such as {@code a = ? and b = ? and ...}
    	 *
    	 * @param sql the sql order on which to append the clause
    	 * @param conditionColumns columns of the where
    	 * @param <T> type of the table
    	 * @return an object that contains indexes and parameter binders of the where
    	 */
    	private <T extends Table> ParameterizedWhere<T> appendWhere(DDLAppender sql, Iterable<? extends Column<T, ?>> conditionColumns) {
    		ParameterizedWhere<T> result = new ParameterizedWhere<>();
    		MutableInt positionCounter = new MutableInt(1);
    		Iterables.stream(conditionColumns).forEach(column -> {
    			sql.cat(column, EQUAL_SQL_PARAMETER_MARK_AND);
    			result.indexesPerColumn.put(column, new int[] { positionCounter.getValue() });
    			positionCounter.increment();
    			result.parameterBinders.put(column, columnBinderRegistry.getBinder(column));
    		});
    		return result;
    	}

    	/**
    	 * Appends a where condition (without "where" keyword) to a given sql order.
    	 * Conditions are appended with the form of tuples such as {@code (a, b) in ((?, ?), (?, ?), ...)}
    	 *
    	 * @param sql the sql order on which to append the clause
    	 * @param conditionColumns columns of the where
    	 * @param whereValuesCount expected number of tuples
    	 * @param <T> type of the table
    	 * @return an object that contains indexes and parameter binders of the where
    	 */
    	public <T extends Table> ParameterizedWhere<T> appendTupledWhere(DDLAppender sql, Collection<Column<T, ?>> conditionColumns, int whereValuesCount) {
    		ParameterizedWhere<T> result = new ParameterizedWhere<>();
    		boolean isComposedKey = conditionColumns.size() > 1;
    		sql.catIf(isComposedKey, "(")
    				.ccat(conditionColumns, ", ")
    				.catIf(isComposedKey, ")");
    		sql.cat(" in (");
    		StringBuilder repeat = Strings.repeat(conditionColumns.size(), SQL_PARAMETER_MARK_1, SQL_PARAMETER_MARK_100, SQL_PARAMETER_MARK_10);
    		repeat.setLength(repeat.length() - 2);
    		String keyMarks = repeat.toString();
    		for (int i = 1; i <= whereValuesCount; i++) {
    			sql.catIf(isComposedKey, "(").cat(keyMarks);
    			sql.catIf(isComposedKey, ")").cat(", ");
    			// because statement indexes start at 0, we must decrement index of 1
    			int startKeyMarkIndex = i-1;
    			MutableInt pkIndex = new MutableInt();
    			conditionColumns.forEach(keyColumn -> {
    				int pkColumnIndex = startKeyMarkIndex * conditionColumns.size() + pkIndex.increment();
    				result.indexesPerColumn.computeIfAbsent(keyColumn, k -> new int[whereValuesCount])[startKeyMarkIndex] = pkColumnIndex;
    			});
    		}
    		conditionColumns.forEach(keyColumn -> result.parameterBinders.put(keyColumn, columnBinderRegistry.getBinder(keyColumn)));
    		sql.cutTail(2).cat(")");
    		return result;
    	}

    	public static class ParameterizedWhere<T extends Table> {

    		private final Map<Column<T, ?>, int[]> indexesPerColumn = new HashMap<>();

    		private final Map<Column<T, ?>, ParameterBinder<?>> parameterBinders = new HashMap<>();

    		public Map<Column<T, ?>, int[]> getColumnToIndex() {
    			return indexesPerColumn;
    		}

    		public Map<Column<T, ?>, ParameterBinder<?>> getParameterBinders() {
    			return parameterBinders;
    		}

    	}

    	private static class ColumnNameComparator implements Comparator<Column> {

    		private static final ColumnNameComparator INSTANCE = new ColumnNameComparator();

    		@Override
    		public int compare(Column o1, Column o2) {
    			return String.CASE_INSENSITIVE_ORDER.compare(o1.getAbsoluteName(), o2.getAbsoluteName());
    		}
    	}
    }